# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the MIT license.
# See the LICENSE file in the project root for more information.

from pathlib import Path
from textwrap import indent
from typing import Sequence, Tuple

from ..commonlib.collection_util import is_empty, partition
from ..commonlib.config import DOCS_PATH
from ..commonlib.util import update_file

from .aggregate_stats import AGGREGATE_FLOAT_STATISTICS
from .run_metrics import ALL_RUN_METRICS
from .single_gc_metrics import ALL_SINGLE_GC_METRICS
from .single_heap_metrics import ALL_SINGLE_HEAP_METRICS
from .types import MetricBase, MetricType


_METRICS_MD_PATH: Path = DOCS_PATH / "metrics.md"


def update_metrics_md() -> None:
    update_file(_METRICS_MD_PATH, _generate_text())


def _generate_text() -> str:
    pairs: Sequence[Tuple[str, Sequence[MetricBase]]] = (
        ("single-heap-metrics", ALL_SINGLE_HEAP_METRICS),
        ("single-gc-metrics", ALL_SINGLE_GC_METRICS),
        ("run-metrics", ALL_RUN_METRICS),
    )
    all_metrics_str = "\n\n".join(_describe_all_metrics(*pair) for pair in pairs)
    return f"{_HEADER}\n\n{all_metrics_str}\n"


_HEADER = f"""
(This file is generated by `py . lint`)

The following list does not include aggregate metrics.

For any single-heap-metric, you can add an underscore `_` followed by an aggregate name
to get a single-gc-metric, for each aggregate name in {tuple(AGGREGATE_FLOAT_STATISTICS.keys())}.
For example, the single-gc-metric `Gen0SizeAfterMB_Mean` is the mean of the single-heap-metric
`Gen0SizeAfterMB` taken from each heap within that GC.

You can similarly get a run metric by aggregating GC metrics, even if that was already an aggregate,
as in `Gen0SizeAfterMB_Mean_Mean`, which is the mean of the single-heap-metric `Gen0SizeAfterMB`,
averaged over each heap within a GC, averaged over all GCs.

Also, while taking an aggregate, you can add `Where` followed by the name of a boolean metric.
For example, the run-metric `PauseDurationMSec_MeanWhereIsBlockingGen2` is the mean of
the single-gc-metric `PauseDurationMSec`,
considering only the GCs where another single-gc-metric `WhereIsBlockingGen2` returns true.

You can also aggregate a boolean metric by prefixing it with `Pct`.
For example, `PctIsBlockingGen2` is a run-metric aggregating the single-gc-metric
`IsBlockingGen2`, giving the percentage of GCs where that returned true.
A boolean aggregate can have a `Where` part too, as in `PctIsNonConcurrentWhereIsGen2`,
which tells you what percentage of Gen2 gcs where blocking.
""".strip()


def _describe_all_metrics(kind: str, metrics: Sequence[MetricBase]) -> str:
    sorted_metrics = sorted(m for m in metrics if not m.is_aggregate)
    for metric in sorted_metrics:
        assert metric.type in (MetricType.bool, MetricType.float)
    bool_metrics, float_metrics = partition(lambda m: m.type == MetricType.bool, sorted_metrics)
    bool_str = _describe_metrics_of_type("bool", bool_metrics)
    float_str = _describe_metrics_of_type("float", float_metrics)
    return f"\n# {kind}\n\n{bool_str}\n\n{float_str}"


def _describe_metrics_of_type(type_name: str, metrics: Sequence[MetricBase]) -> str:
    if is_empty(metrics):
        return f"no {type_name} metrics"
    else:
        test_status_metrics, other_metrics = partition(lambda m: m.is_from_test_status, metrics)
        test_status_str = _describe_filtered_metrics(test_status_metrics)
        other_str = "\n".join(_describe_metric(m) for m in other_metrics)

        if is_empty(test_status_metrics):
            metrics_str = other_str
        else:
            metrics_str = (
                f"### {type_name} metrics that only require test status\n\n{test_status_str}"
                + f"\n\n### {type_name} metrics that require a trace file\n\n{other_str}"
            )

        return f"## {type_name} metrics\n\n{metrics_str}"


def _describe_filtered_metrics(metrics: Sequence[MetricBase]) -> str:
    return "\n".join(_describe_metric(m) for m in metrics)


def _describe_metric(m: MetricBase) -> str:
    if m.doc:
        doc = indent(m.doc, "\t")
        return f"{m.name}\n{doc}"
    else:
        return m.name
